Odkryj branded types w TypeScript, pot臋偶n膮 technik臋 do osi膮gania typowania nominalnego w strukturalnym systemie typ贸w. Dowiedz si臋, jak zwi臋kszy膰 bezpiecze艅stwo typ贸w i klarowno艣膰 kodu.
Branded Types w TypeScript: Typowanie nominalne w systemie strukturalnym
Strukturalny system typ贸w TypeScript oferuje elastyczno艣膰, ale czasami mo偶e prowadzi膰 do nieoczekiwanych zachowa艅. Branded types (typy naznaczone) dostarczaj膮 sposobu na wymuszenie typowania nominalnego, zwi臋kszaj膮c bezpiecze艅stwo typ贸w i klarowno艣膰 kodu. Ten artyku艂 szczeg贸艂owo omawia branded types, dostarczaj膮c praktycznych przyk艂ad贸w i najlepszych praktyk ich implementacji.
Zrozumienie typowania strukturalnego a nominalnego
Zanim zag艂臋bimy si臋 w branded types, wyja艣nijmy r贸偶nic臋 mi臋dzy typowaniem strukturalnym a nominalnym.
Typowanie strukturalne (Duck Typing)
W systemie typowania strukturalnego dwa typy s膮 uwa偶ane za kompatybilne, je艣li maj膮 t臋 sam膮 struktur臋 (tj. te same w艂a艣ciwo艣ci o tych samych typach). TypeScript u偶ywa typowania strukturalnego. Rozwa偶 ten przyk艂ad:
interface Point {
x: number;
y: number;
}
interface Vector {
x: number;
y: number;
}
const point: Point = { x: 10, y: 20 };
const vector: Vector = point; // Valid in TypeScript
console.log(vector.x); // Output: 10
Mimo 偶e Point
i Vector
s膮 zadeklarowane jako odr臋bne typy, TypeScript pozwala przypisa膰 obiekt Point
do zmiennej Vector
, poniewa偶 maj膮 t臋 sam膮 struktur臋. Mo偶e to by膰 wygodne, ale mo偶e r贸wnie偶 prowadzi膰 do b艂臋d贸w, gdy trzeba rozr贸偶ni膰 logicznie r贸偶ne typy, kt贸re przypadkowo maj膮 ten sam kszta艂t. Na przyk艂ad, my艣l膮c o wsp贸艂rz臋dnych geograficznych (d艂ugo艣膰/szeroko艣膰), kt贸re mog膮 przypadkowo pasowa膰 do wsp贸艂rz臋dnych pikseli na ekranie.
Typowanie nominalne
W systemie typowania nominalnego typy s膮 uwa偶ane za kompatybilne tylko wtedy, gdy maj膮 t臋 sam膮 nazw臋. Nawet je艣li dwa typy maj膮 t臋 sam膮 struktur臋, s膮 traktowane jako odr臋bne, je艣li maj膮 r贸偶ne nazwy. J臋zyki takie jak Java i C# u偶ywaj膮 typowania nominalnego.
Potrzeba stosowania Branded Types
Typowanie strukturalne w TypeScript mo偶e by膰 problematyczne, gdy trzeba zapewni膰, 偶e warto艣膰 nale偶y do okre艣lonego typu, niezale偶nie od jej struktury. Na przyk艂ad, rozwa偶my reprezentowanie walut. Mo偶esz mie膰 r贸偶ne typy dla USD i EUR, ale oba mog膮 by膰 reprezentowane jako liczby. Bez mechanizmu do ich rozr贸偶niania, mo偶na by przypadkowo wykona膰 operacje na niew艂a艣ciwej walucie.
Branded types rozwi膮zuj膮 ten problem, pozwalaj膮c na tworzenie odr臋bnych typ贸w, kt贸re s膮 strukturalnie podobne, ale traktowane jako r贸偶ne przez system typ贸w. Zwi臋ksza to bezpiecze艅stwo typ贸w i zapobiega b艂臋dom, kt贸re w innym przypadku mog艂yby zosta膰 przeoczone.
Implementacja Branded Types w TypeScript
Branded types s膮 implementowane przy u偶yciu typ贸w przeci臋cia (intersection types) oraz unikalnego symbolu lub litera艂u ci膮gu znak贸w. Chodzi o to, aby doda膰 "znak" (brand) do typu, kt贸ry odr贸偶nia go od innych typ贸w o tej samej strukturze.
U偶ywanie symboli (zalecane)
U偶ywanie symboli do "brandowania" jest og贸lnie preferowane, poniewa偶 symbole maj膮 gwarancj臋 unikalno艣ci.
const USD = Symbol('USD');
type USD = number & { readonly [USD]: unique symbol };
const EUR = Symbol('EUR');
type EUR = number & { readonly [EUR]: unique symbol };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);
W tym przyk艂adzie USD
i EUR
to typy naznaczone oparte na typie number
. unique symbol
zapewnia, 偶e te typy s膮 odr臋bne. Funkcje createUSD
i createEUR
s艂u偶膮 do tworzenia warto艣ci tych typ贸w, a funkcja addUSD
akceptuje tylko warto艣ci USD
. Pr贸ba dodania warto艣ci EUR
do warto艣ci USD
spowoduje b艂膮d typu.
U偶ywanie litera艂贸w ci膮gu znak贸w
Mo偶na r贸wnie偶 u偶ywa膰 litera艂贸w ci膮gu znak贸w do "brandowania", chocia偶 to podej艣cie jest mniej solidne ni偶 u偶ywanie symboli, poniewa偶 litera艂y ci膮g贸w znak贸w nie maj膮 gwarancji unikalno艣ci.
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };
function createUSD(value: number): USD {
return value as USD;
}
function createEUR(value: number): EUR {
return value as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(10);
const usd2 = createUSD(20);
const eur1 = createEUR(15);
const totalUSD = addUSD(usd1, usd2);
console.log("Total USD:", totalUSD);
// Uncommenting the next line will cause a type error
// const invalidOperation = addUSD(usd1, eur1);
Ten przyk艂ad osi膮ga ten sam rezultat co poprzedni, ale u偶ywa litera艂贸w ci膮gu znak贸w zamiast symboli. Chocia偶 jest to prostsze, wa偶ne jest, aby upewni膰 si臋, 偶e litera艂y u偶ywane do "brandowania" s膮 unikalne w obr臋bie Twojej bazy kodu.
Praktyczne przyk艂ady i przypadki u偶ycia
Branded types mo偶na zastosowa膰 w r贸偶nych scenariuszach, w kt贸rych trzeba wymusi膰 bezpiecze艅stwo typ贸w wykraczaj膮ce poza kompatybilno艣膰 strukturaln膮.
Identyfikatory (ID)
Rozwa偶my system z r贸偶nymi typami identyfikator贸w, takimi jak UserID
, ProductID
i OrderID
. Wszystkie te identyfikatory mog膮 by膰 reprezentowane jako liczby lub ci膮gi znak贸w, ale chcemy zapobiec przypadkowemu mieszaniu r贸偶nych typ贸w ID.
const UserIDBrand = Symbol('UserID');
type UserID = string & { readonly [UserIDBrand]: unique symbol };
const ProductIDBrand = Symbol('ProductID');
type ProductID = string & { readonly [ProductIDBrand]: unique symbol };
function getUser(id: UserID): { name: string } {
// ... fetch user data
return { name: "Alice" };
}
function getProduct(id: ProductID): { name: string, price: number } {
// ... fetch product data
return { name: "Example Product", price: 25 };
}
function createUserID(id: string): UserID {
return id as UserID;
}
function createProductID(id: string): ProductID {
return id as ProductID;
}
const userID = createUserID('user123');
const productID = createProductID('product456');
const user = getUser(userID);
const product = getProduct(productID);
console.log("User:", user);
console.log("Product:", product);
// Uncommenting the next line will cause a type error
// const invalidCall = getUser(productID);
Ten przyk艂ad pokazuje, jak branded types mog膮 zapobiec przekazaniu ProductID
do funkcji, kt贸ra oczekuje UserID
, zwi臋kszaj膮c bezpiecze艅stwo typ贸w.
Warto艣ci specyficzne dla domeny
Branded types mog膮 by膰 r贸wnie偶 przydatne do reprezentowania warto艣ci specyficznych dla domeny z ograniczeniami. Na przyk艂ad, mo偶esz mie膰 typ dla warto艣ci procentowych, kt贸re zawsze powinny mie艣ci膰 si臋 w zakresie od 0 do 100.
const PercentageBrand = Symbol('Percentage');
type Percentage = number & { readonly [PercentageBrand]: unique symbol };
function createPercentage(value: number): Percentage {
if (value < 0 || value > 100) {
throw new Error('Percentage must be between 0 and 100');
}
return value as Percentage;
}
function applyDiscount(price: number, discount: Percentage): number {
return price * (1 - discount / 100);
}
try {
const discount = createPercentage(20);
const discountedPrice = applyDiscount(100, discount);
console.log("Discounted Price:", discountedPrice);
// Uncommenting the next line will cause an error during runtime
// const invalidPercentage = createPercentage(120);
} catch (error) {
console.error(error);
}
Ten przyk艂ad pokazuje, jak wymusi膰 ograniczenie na warto艣ci typu naznaczonego w czasie wykonania. Chocia偶 system typ贸w nie mo偶e zagwarantowa膰, 偶e warto艣膰 Percentage
zawsze b臋dzie mi臋dzy 0 a 100, funkcja createPercentage
mo偶e wymusi膰 to ograniczenie w czasie wykonania. Mo偶na r贸wnie偶 u偶y膰 bibliotek, takich jak io-ts, do wymuszania walidacji typ贸w nazwanych w czasie wykonania.
Reprezentacje daty i czasu
Praca z datami i godzinami mo偶e by膰 trudna ze wzgl臋du na r贸偶ne formaty i strefy czasowe. Branded types mog膮 pom贸c w rozr贸偶nieniu r贸偶nych reprezentacji daty i czasu.
const UTCDateBrand = Symbol('UTCDate');
type UTCDate = string & { readonly [UTCDateBrand]: unique symbol };
const LocalDateBrand = Symbol('LocalDate');
type LocalDate = string & { readonly [LocalDateBrand]: unique symbol };
function createUTCDate(dateString: string): UTCDate {
// Validate that the date string is in UTC format (e.g., ISO 8601 with Z)
if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(dateString)) {
throw new Error('Invalid UTC date format');
}
return dateString as UTCDate;
}
function createLocalDate(dateString: string): LocalDate {
// Validate that the date string is in local date format (e.g., YYYY-MM-DD)
if (!/\d{4}-\d{2}-\d{2}/.test(dateString)) {
throw new Error('Invalid local date format');
}
return dateString as LocalDate;
}
function convertUTCDateToLocalDate(utcDate: UTCDate): LocalDate {
// Perform time zone conversion
const date = new Date(utcDate);
const localDateString = date.toLocaleDateString();
return createLocalDate(localDateString);
}
try {
const utcDate = createUTCDate('2024-01-20T10:00:00.000Z');
const localDate = convertUTCDateToLocalDate(utcDate);
console.log("UTC Date:", utcDate);
console.log("Local Date:", localDate);
} catch (error) {
console.error(error);
}
Ten przyk艂ad rozr贸偶nia daty UTC i lokalne, zapewniaj膮c, 偶e pracujesz z poprawn膮 reprezentacj膮 daty i czasu w r贸偶nych cz臋艣ciach aplikacji. Walidacja w czasie wykonania zapewnia, 偶e tylko poprawnie sformatowane ci膮gi znak贸w daty mog膮 by膰 przypisane do tych typ贸w.
Dobre praktyki u偶ywania Branded Types
Aby efektywnie u偶ywa膰 branded types w TypeScript, rozwa偶 nast臋puj膮ce dobre praktyki:
- U偶ywaj symboli do brandowania: Symbole zapewniaj膮 najsilniejsz膮 gwarancj臋 unikalno艣ci, zmniejszaj膮c ryzyko b艂臋d贸w typ贸w.
- Tw贸rz funkcje pomocnicze: U偶ywaj funkcji pomocniczych do tworzenia warto艣ci typ贸w nazwanych. Zapewnia to centralny punkt walidacji i gwarantuje sp贸jno艣膰.
- Stosuj walidacj臋 w czasie wykonania: Chocia偶 branded types zwi臋kszaj膮 bezpiecze艅stwo typ贸w, nie zapobiegaj膮 przypisywaniu nieprawid艂owych warto艣ci w czasie wykonania. U偶ywaj walidacji w czasie wykonania, aby wymusi膰 ograniczenia.
- Dokumentuj Branded Types: Jasno dokumentuj cel i ograniczenia ka偶dego typu naznaczonego, aby poprawi膰 utrzymywalno艣膰 kodu.
- Rozwa偶 implikacje wydajno艣ciowe: Branded types wprowadzaj膮 niewielki narzut z powodu typu przeci臋cia i potrzeby stosowania funkcji pomocniczych. Rozwa偶 wp艂yw na wydajno艣膰 w krytycznych sekcjach kodu.
Zalety Branded Types
- Zwi臋kszone bezpiecze艅stwo typ贸w: Zapobiega przypadkowemu mieszaniu typ贸w strukturalnie podobnych, ale logicznie r贸偶nych.
- Poprawiona klarowno艣膰 kodu: Czyni kod bardziej czytelnym i 艂atwiejszym do zrozumienia poprzez jawne rozr贸偶nianie typ贸w.
- Mniej b艂臋d贸w: Wy艂apuje potencjalne b艂臋dy w czasie kompilacji, zmniejszaj膮c ryzyko b艂臋d贸w w czasie wykonania.
- Zwi臋kszona utrzymywalno艣膰: U艂atwia utrzymanie i refaktoryzacj臋 kodu poprzez wyra藕ne oddzielenie odpowiedzialno艣ci.
Wady Branded Types
- Zwi臋kszona z艂o偶ono艣膰: Dodaje z艂o偶ono艣ci do bazy kodu, zw艂aszcza przy du偶ej liczbie typ贸w nazwanych.
- Narzut w czasie wykonania: Wprowadza niewielki narzut w czasie wykonania z powodu potrzeby funkcji pomocniczych i walidacji.
- Potencjalny boilerplate: Mo偶e prowadzi膰 do powtarzalnego kodu (boilerplate), zw艂aszcza przy tworzeniu i walidacji typ贸w nazwanych.
Alternatywy dla Branded Types
Chocia偶 branded types s膮 pot臋偶n膮 technik膮 do osi膮gania typowania nominalnego w TypeScript, istniej膮 alternatywne podej艣cia, kt贸re mo偶na rozwa偶y膰.
Typy nieprzezroczyste (Opaque Types)
Typy nieprzezroczyste s膮 podobne do branded types, ale zapewniaj膮 bardziej jawny spos贸b na ukrycie typu bazowego. TypeScript nie ma wbudowanego wsparcia dla typ贸w nieprzezroczystych, ale mo偶na je symulowa膰 za pomoc膮 modu艂贸w i prywatnych symboli.
Klasy
U偶ycie klas mo偶e zapewni膰 bardziej zorientowane obiektowo podej艣cie do definiowania odr臋bnych typ贸w. Chocia偶 klasy w TypeScript s膮 typowane strukturalnie, oferuj膮 ja艣niejsze oddzielenie odpowiedzialno艣ci i mog膮 by膰 u偶ywane do wymuszania ogranicze艅 za pomoc膮 metod.
Biblioteki takie jak `io-ts` czy `zod`
Te biblioteki zapewniaj膮 zaawansowan膮 walidacj臋 typ贸w w czasie wykonania i mog膮 by膰 艂膮czone z branded types, aby zapewni膰 bezpiecze艅stwo zar贸wno w czasie kompilacji, jak i w czasie wykonania.
Podsumowanie
Branded types w TypeScript to cenne narz臋dzie do zwi臋kszania bezpiecze艅stwa typ贸w i klarowno艣ci kodu w systemie typowania strukturalnego. Dodaj膮c "znak" do typu, mo偶na wymusi膰 typowanie nominalne i zapobiec przypadkowemu mieszaniu typ贸w strukturalnie podobnych, ale logicznie r贸偶nych. Chocia偶 branded types wprowadzaj膮 pewn膮 z艂o偶ono艣膰 i narzut, korzy艣ci p艂yn膮ce z poprawy bezpiecze艅stwa typ贸w i utrzymywalno艣ci kodu cz臋sto przewa偶aj膮 nad wadami. Rozwa偶 u偶ycie branded types w scenariuszach, w kt贸rych musisz zapewni膰, 偶e warto艣膰 nale偶y do okre艣lonego typu, niezale偶nie od jej struktury.
Rozumiej膮c zasady typowania strukturalnego i nominalnego oraz stosuj膮c najlepsze praktyki przedstawione w tym artykule, mo偶esz skutecznie wykorzysta膰 branded types do pisania bardziej solidnego i 艂atwiejszego w utrzymaniu kodu TypeScript. Od reprezentowania walut i identyfikator贸w po wymuszanie ogranicze艅 specyficznych dla domeny, branded types zapewniaj膮 elastyczny i pot臋偶ny mechanizm do zwi臋kszania bezpiecze艅stwa typ贸w w Twoich projektach.
Pracuj膮c z TypeScript, odkrywaj r贸偶ne techniki i biblioteki dost臋pne do walidacji i egzekwowania typ贸w. Rozwa偶 u偶ycie branded types w po艂膮czeniu z bibliotekami do walidacji w czasie wykonania, takimi jak io-ts
czy zod
, aby osi膮gn膮膰 kompleksowe podej艣cie do bezpiecze艅stwa typ贸w.